package curvilinear;

import ij.ImagePlus;

import java.awt.Graphics2D;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;

import javax.swing.JFrame;

import model.CurvilinearSelectionModel;
import model.ProfileModel;
import profile.Profile;
import profile.ProfileResultFrame;
import util.QualityControlHelper;
import controller.ProfileController;

/**
 * Utility methods for Curvilinear Profile plugin
 */
public class CurvilinearHelper {
    
    
    public static void getProfile(CurvilinearSelectionModel selectionModel, ImagePlus img, ProfileController profileController, ProfileModel profileModel) {
        Point2D.Float circleCenter = selectionModel.getCircleCenter();
        Point2D.Float minRadiusPoint = selectionModel.getMinRadiusPoint();
        Point2D.Float maxRadiusPoint = selectionModel.getMaxRadiusPoint();
        
        float radiusROIInner = (float) Math.sqrt(Math.pow(minRadiusPoint.x - circleCenter.x, 2)
                + Math.pow(minRadiusPoint.y - circleCenter.y, 2));
        float radiusROIOuter = (float) Math.sqrt(Math.pow(maxRadiusPoint.x - circleCenter.x, 2)
                + Math.pow(maxRadiusPoint.y - circleCenter.y, 2));
        int width = Math.round(radiusROIOuter - radiusROIInner);

        Point2D.Float minIntersection = CurvilinearHelper.findIntersection(radiusROIInner, selectionModel.getMinWidthPoint(), circleCenter, true);
        Point2D.Float maxIntersection = CurvilinearHelper.findIntersection(radiusROIInner, selectionModel.getMaxWidthPoint(), circleCenter, false);

        int numRays = (int) Math.floor(maxIntersection.x - minIntersection.x);
        float[] xBinsMin = new float[numRays];
        // populate x values
        for (int i = 0; i < xBinsMin.length; i++) {
            xBinsMin[i] = minIntersection.x + i;
        }

        float[] yInnerArc = new float[numRays];
        // solve for y values of arc based on xBinsMin
        for (int i = 0; i < xBinsMin.length; i++) {
            yInnerArc[i] = (circleCenter.y + (float) (Math.sqrt(Math.pow(radiusROIInner, 2)
                    - Math.pow(xBinsMin[i] - circleCenter.x, 2))));
        }

        float[] avgIntensity = new float[numRays];
        float[] xBinsMax = new float[numRays];
        float[] yOuterArc = new float[numRays];
        BufferedImage bimg = img.getBufferedImage();
        for (int i = 0; i < numRays; i++) {
            float sum = 0;
            float m = (yInnerArc[i] - circleCenter.y) / (xBinsMin[i] - circleCenter.x);
            float mag = (float) Math.sqrt(1 + Math.pow(m, 2));

            float xN = 1.0f / mag;
            float yN = m / mag;
            if (m < 0) {
                xN *= -1.0f;
                yN *= -1.0f;
            }

            float xA = xBinsMin[i];
            float yA = yInnerArc[i];

            int counter = 0;
            for (int r = 0; r < width; r++) {
                float tempX = xA + r * xN;
                float tempY = yA + r * yN;
                // getRGB returns the int RGB value, but since we are using a grayscale image,
                // we only need to look at one of R, G, or B (here we are using B)
                float tempVal = bimg.getRGB(Math.round(tempX), Math.round(tempY)) & 255;
                sum += tempVal;
                counter++;
                if (counter == (width - 1)) {
                    xBinsMax[i] = tempX;
                    yOuterArc[i] = tempY;
                    Graphics2D g = (Graphics2D) bimg.getGraphics();
                    if (i == 0) {
                        g.drawLine((int) xBinsMin[i], (int) yInnerArc[i], (int) xBinsMax[i], (int) yOuterArc[i]);
                    } else if (i > 0 && i < (numRays - 1)) {
                        g.drawLine((int) xBinsMin[i], (int) yInnerArc[i], (int) xBinsMin[i - 1], (int) yInnerArc[i - 1]);
                        g.drawLine((int) xBinsMax[i], (int) yOuterArc[i], (int) xBinsMax[i - 1], (int) yOuterArc[i - 1]);
                    } else if (i == (numRays - 1)) {
                        g.drawLine((int) xBinsMin[i], (int) yInnerArc[i], (int) xBinsMax[i], (int) yOuterArc[i]);
                    }
                }
            }

            avgIntensity[i] = sum / counter;
        }

        String filename = QualityControlHelper.getFileName(img);
        Profile profile = new Profile(avgIntensity, img, bimg);
        JFrame resultFrame = new ProfileResultFrame(profile, profileController, profileModel, filename);
        resultFrame.setVisible(true);

    }
    
    
    /**
     * This is used to find the intersection between a line and a point on the
     * radius of a circle.  In our case, we are using it to find where the user's
     * width selection meets the user's radius selection
     * 
     * @param radiusROIInner
     * @param point
     * @param circleCenter
     * @param min
     * @return
     */
    public static Point2D.Float findIntersection(float radiusROIInner, Point2D.Float point, Point2D.Float circleCenter, boolean min) {
        float m = (point.y - circleCenter.y) / (point.x - circleCenter.x);

        Point2D.Float result = new Point2D.Float();

        // For detailed information on the equations and methods used here, see
        // this paper:
        // https://docs.google.com/viewer?url=http%3A%2F%2Fwww.ferris.edu%2Ffaculty%2Fburtchr%2Fsure215%2Fnotes%2Fline-circle.pdf
        float h = circleCenter.x;
        float k = circleCenter.y;
        float x1 = point.x;
        float y1 = point.y;

        float a = (float) (Math.pow(m, 2) + 1);
        float b = (float) ((-2.0f) * (h + (Math.pow(m, 2) * x1) - (m * y1) + (m * k)));
        float c = (float) (Math.pow(h, 2) + Math.pow(k, 2) +
                          Math.pow(y1, 2) + Math.pow(m, 2) * Math.pow(x1, 2) -
                          Math.pow(radiusROIInner, 2) - (2 * m * x1 * y1) +
                          (2 * k * m * x1) - (2 * k * y1));

        result.x = quadSolve(a, b, c, min);
        result.y = m * (result.x - x1) + y1;

        return result;
    }
    
    /**
     * Quadratic solver.  Parameter <code>min</code> specifies whether we want
     * to return the minimum answer or not.
     * 
     * @param a
     * @param b
     * @param c
     * @param min
     * @return
     */
    private static float quadSolve(float a, float b, float c, boolean min) {
        // Solve the discriminant
        float discr = (float) (b * b) - (4 * a * c);

        // Test for NaN
        if (Float.isNaN(discr)) {
            System.out.println("Equation has no roots");
        }

        float root1 = 0, root2 = 0;
        if (discr > 0) {
            root1 = (float) (-b / 2.0 / a - (float) Math.sqrt(discr) /
                    2.0 / a);
            root2 = (float) (-b / 2.0 / a + (float) Math.sqrt(discr) /
                                2.0 / a);
        }

        if (discr == 0) {
            root1 = (-b + discr) / 2 * a;
        }

        if (discr < 0)
            System.out.println("Equation has imaginary roots");
        float fmin = (root1 < root2) ? root1 : root2;
        float fmax = (root1 > root2) ? root1 : root2;

        if (min) {
            return fmin;
        }
        return fmax;
    }
}
